#!/usr/bin/env python

'''
  MUDServ - A Multi-User Dungeon Server
  A Pygame-based Client
  
  Author: Michael Chest
  
  Copyright (c) 2009, Ecl3ctic Software
  All rights reserved.
  
  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are met:

  * Redistributions of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer.
  * Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.
  * Neither the name of Ecl3ctic Software nor the names of its contributors
    may be used to endorse or promote products derived from this software
    without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  POSSIBILITY OF SUCH DAMAGE.
'''

import socket
import sys
import logging
import platform
import datetime
import simplejson
import threading
import pygame
import math
import random
from pygame import *
from time import sleep
from clientlib import *

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(levelname)s %(message)s',
                    filename='mudserv.log')

# Client version string
__version__ = '04-11-09'
__author__ = 'Michael Chest'

consoleCommands = ['tell', 'who', 'version', 'motd', 'setname', 'wall']
nMessages = 4     # Number of lines for the message buffer
username = ''
size = 1

imgMap = {}
fileNameMap = {}
debug = False


def makeIndicator(dotSize):
  isize = dotSize + (dotSize + 2) * 2
  result = []
  for y in xrange(isize):
    tmpAry = []
    for x in xrange(isize):
      tmpAry.append(0)
    result.append(tmpAry)
  
  t = p = (isize - 1) / 2.0
  while p > dotSize:
    d2 = math.sqrt(p * p)
    for i, y in enumerate(result):
      for j, x in enumerate(y):
        if abs((math.sqrt((i - t)*(i - t) + (j - t)*(j - t))) - d2) < 0.5:
          result[i][j] = 1
    p = p - 2.0

  return result

def convertAryToSurface(ary, screen):
  surf = Surface((len(ary), len(ary[0])), SRCALPHA, screen)
  for i, y in enumerate(ary):
    for j, x in enumerate(y):
      if x == 1:
        draw.rect(surf, (0, 255, 0), Rect(i, j, 1, 1), 1)
      else:
        draw.rect(surf, Color(0, 0, 0, 0), Rect(i, j, 1, 1), 1)
  return surf

def loadImg(num):
  if num == -1:
    return floor
  else:
    return wall

def interpretBoardData(fileData):
  dataStructure = []
  curLine = []
  for c in fileData:
    if c == ' ' or c == '+' or c == 'S':
      curLine.append(-1)
    elif c == '#':
      curLine.append(1)
    elif c == '\n':
      dataStructure.append(curLine[:-1])
      curLine = []
  return dataStructure

def drawBoard(surf, pos, struct):
  for y, line in enumerate(struct):
    for x, ele in enumerate(line):
      surf.blit(loadImg(struct[y][x]), (pos.left + x * size, pos.top + y * size))

def quitPretty():
  try:
    executeCommand(sock, 'quit')
  except:
    pass
  sock.close()

class BackgroundLoader(threading.Thread):
  def __init__(self, sock, screen):
    threading.Thread.__init__(self)
    self.sock = sock
    self.screen = screen
    self.surfii = {}
    self.edgePos = []
    self.numSinceLoad = 0
  def get(self, pos):
    if pos in self.surfii:
      return self.surfii[pos]
    else:
      return None
  def getLoadedPositions(self):
    return self.surfii.keys()[:]
  def load(self, pos):
    loadedSomething = False
    anyAdded = 0
    for nx in [-1, 0, 1]:
      for ny in [-1, 0, 1]:
        newPos = pos[0] + nx, pos[1] + ny, pos[2]
        if newPos not in self.surfii:
          anyAdded += 1
          newSurface = Surface((bx, by), 0, self.screen)
          fd = executeCommand(self.sock, 'getasciiboard', [str(newPos[0]), str(newPos[1]), str(newPos[2]), 'False'])
          if int(fd[0]) == 0:
            drawBoard(newSurface, Rect(0, 0, bx, by), interpretBoardData(fd[1]))
            self.surfii[newPos] = newSurface
            tmpList = [newPos]
            tmpList.extend(self.edgePos)
            self.edgePos = tmpList
            loadedSomething = True
            anyAdded -= 1
    if anyAdded == 0:
      self.edgePos.remove(pos)
    if loadedSomething:
      self.numSinceLoad = 0
    else:
      self.numSinceLoad += 1
    return loadedSomething
  def run(self):
    try:
      while not killMe:
        sleep(1.0)
        if self.edgePos and self.numSinceLoad < 5:
          randPos = self.edgePos[0]
          if debug:
            print "Loading Edge", randPos
            baseX = (-locx + randPos[0]) * bx
            baseY = height - (-locy + randPos[1]) * by
            draw.lines(self.screen, (25, 10, 25), 1, [(baseX, baseY), (baseX + bx - 1, baseY), (baseX + bx - 1, baseY + by - 1), (baseX, baseY + by - 1)], 1)
          self.load(randPos)
          if randPos in self.edgePos:
            self.edgePos.remove(randPos)
            self.edgePos.append(randPos)
        else:
          randPos = (random.randrange(-(xPerScreen / 2), xPerScreen / 2), random.randrange(-(yPerScreen / 2), yPerScreen / 2), 0)
          if debug:
            print "Loading Random", randPos
            baseX = (-locx + randPos[0]) * bx
            baseY = height - (-locy + randPos[1]) * by
            draw.lines(self.screen, (10, 25, 10), 1, [(baseX, baseY), (baseX + bx - 1, baseY), (baseX + bx - 1, baseY + by - 1), (baseX, baseY + by - 1)], 1)
          self.edgePos.append(randPos)
          if not self.load(randPos) and randPos in self.edgePos:
            self.edgePos.remove(randPos)
          else:
            if debug:
              print "Shuffling edges"
            random.shuffle(self.edgePos)
    except socket.error:
      print "Error communicating with server. Map Updater Thread exiting."
    except:
      print "Error: Map Updater Thread exiting."
      raise
    print "Map Updater Thread dieing.  Player Updater Thread should die next."

class PlayerTracker(threading.Thread):
  def __init__(self, sock, screen):
    threading.Thread.__init__(self)
    self.sock = sock
    self.screen = screen
    self.playerMap = {}
    self.bThread = BackgroundLoader(sock, screen)
    self.lastPlayerUpdate = datetime.datetime.now() - datetime.timedelta(seconds=5.1)
    self.lastWhoUpdate = datetime.datetime.now() - datetime.timedelta(seconds=15.1)
  def getLoadedPositions(self):
    return self.bThread.getLoadedPositions()
  def get(self, pos, withLoad=False):
    if withLoad and self.bThread.get(pos) == None:
      self.bThread.load(pos)
    return self.bThread.get(pos)
  def drawPlayersAtPos(self, pos):
    if pos in self.playerMap:
      for username, data in self.playerMap[pos].items():
        #surf = self.bThread.get((data['bx'], data['by'], 0))
        screenX, screenY = (-locx + pos[0]) * bx, height - (-locy + pos[1]) * by
        #self.screen.fill((0, 0, 0), Rect(screenX, screenY, bx, by))
        #self.screen.blit(surf, (screenX, screenY))
        #blitSquare(self.screen, screenX + data['x'] * size, screenY + data['y'] * size, size, playerImg)
        self.screen.blit(playerImg, (screenX + data['x'] * size, screenY + data['y'] * size))
        if data['state']:
          indicatorOffset = 2 + size
          self.screen.blit(indicator, (screenX + min(bx, max(0, data['x'] * size - indicatorOffset)), screenY + min(by, max(0, data['y'] * size - indicatorOffset))))
        data['state'] = not data['state']
  def removeUser(self, username):
    userWasInPos = None
    for pos, userDict in self.playerMap.items():
      if username in userDict:
        userWasInPos = pos
        break
    if userWasInPos != None:
      del self.playerMap[userWasInPos][username]
      return True
    return False
  def run(self):
    try:
      self.bThread.start()
      self.bThread.load((0, 0, 0))
      while self.bThread.isAlive():
        sleep(1.0)
        if self.lastWhoUpdate < datetime.datetime.now() - datetime.timedelta(seconds=15.0):
          self.lastWhoUpdate = datetime.datetime.now()
          self.playerMap = {}
        if self.lastPlayerUpdate < datetime.datetime.now() - datetime.timedelta(seconds=5.0):
          for x in xrange(locx, locx + xPerScreen + 1):
            for y in xrange(locy, locy + yPerScreen + 1):
              curPos = (x, y, 0)
              if self.bThread.get(curPos) != None:
                if curPos not in self.playerMap:
                  self.playerMap[curPos] = {}
                players = executeCommand(self.sock, 'getnearbyplayers', [str(x), str(y), str(0)])
                jsonPlayers = simplejson.loads(players[1])
                for player in jsonPlayers:
                  state = True
                  if player['username'] in self.playerMap[curPos]:
                    state = self.playerMap[curPos][player['username']]['state']
                  self.playerMap[curPos][player['username']] = { 'x': player['x'], 'y': player['y'], 'state': state }
        for pos, data in self.playerMap.items():
          surf = self.bThread.get(pos)
          screenX, screenY = (-locx + pos[0]) * bx, height - (-locy + pos[1]) * by
          self.screen.fill((0, 0, 0), Rect(screenX, screenY, bx, by))
          self.screen.blit(surf, (screenX, screenY))
          for username, playerData in data.items():
            #blitSquare(self.screen, screenX + playerData['x'] * size, screenY + playerData['y'] * size, size, playerImg)
            self.screen.blit(playerImg, (screenX + playerData['x'] * size, screenY + playerData['y'] * size))
            if playerData['state']:
              indicatorOffset = 2 + size
              self.screen.blit(indicator, (screenX + min(bx, max(0, playerData['x'] * size - indicatorOffset)), screenY + min(by, max(0, playerData['y'] * size - indicatorOffset))))
            playerData['state'] = not playerData['state']
    except socket.error:
      print "Error communicating with server. Player Updater Thread exiting."
    except:
      print "Error: Player Updater Thread exiting."
      raise
    killMe = True
    #self.bThread.join()
    print "Player Updater Thread dieing.  Main Program should die next."

print 'Welcome to the Ecl3ctic MUD Pygame World View Client v' + __version__ + '!'
  
# Setup a socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

cla = CommandLineArguments(sys.argv)

print cla.getFlags()

if "d" in cla.getFlags():
  debug = True

for flag in cla.getFlags():
  if "s" in flag:
    size = int(flag[1:])

if size > 10:
  size = 10
if size < 1:
  size = 1

# Get the host and port information
if len(cla.getParams()) < 2:
  host = 'localhost'
else:
  host = cla.getParams()[1]

if len(cla.getParams()) > 2:
  port = int(cla.getParams()[2])
else:
  port = 5656

sys.stdout.write('Connecting to ' + str(host) + ' on port ' + str(port) + '...')
sys.stdout.flush()

# Open a socket
sock.connect((host, port))

sys.stdout.write('\n\n')

error = ''
lastBoard = ''
showConsole = False
consoleInput = ''

# Get server info
print "This server is running version " + executeCommand(sock, 'version')[1]
print

bx, by = map(int, executeCommand(sock, 'getboarddimensions')[1].split())

bx -= 1

bx *= size
by *= size

pygame.init()

dispInfo = display.Info()
MAX_WIDTH, MAX_HEIGHT = min(dispInfo.current_w, 1024), min(dispInfo.current_h, 768)

width, height = MAX_WIDTH - 50, MAX_HEIGHT - 50
width = ((width / bx) * bx)
height = ((height / by) * by)
xPerScreen = width / bx
yPerScreen = height / by
dispSize = width, height
print dispSize
screen = pygame.display.set_mode(dispSize)

playerImg = Surface((size, size), 0, screen)
playerImg.fill((0, 255, 0))
floor = Surface((size, size), 0, screen)
floor.fill((0, 0, 0))
wall = Surface((size, size), 0, screen)
wall.fill((20, 80, 20))
indicator = convertAryToSurface(makeIndicator(size), screen)

# Status Flags
killMe = False

moveAmts = { K_LEFT: (1, 0), K_RIGHT: (-1, 0), K_UP: (0, 1), K_DOWN: (0, -1) }

locx, locy = -(width / bx / 2), -(height / by / 2)
changed = True
lastRefresh = datetime.datetime.now() - datetime.timedelta(seconds=5.0)
pThread = PlayerTracker(sock, screen)

pThread.start()

# Main loop
try:
  while not killMe:
    pygame.time.delay(40)
    
    if changed or lastRefresh < datetime.datetime.now() - datetime.timedelta(seconds=4.0):
      lastRefresh = datetime.datetime.now()
      changed = False
      screen.fill((0, 0, 0))
      for key in pThread.getLoadedPositions():
        screenX, screenY = (-locx + key[0]) * bx, height - (-locy + key[1]) * by
        if screenX >= 0 and screenX <= width - bx and screenY >= 0 and screenY <= height - by:
          screen.blit(pThread.get(key), (screenX, screenY))
          pThread.drawPlayersAtPos(key)

    pygame.display.update()
    for event in pygame.event.get(): #[KEYDOWN, KEYUP, QUIT, MOUSEBUTTONDOWN]):
      if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE) or (event.type == KEYUP and event.mod in (KMOD_META, KMOD_LMETA, KMOD_RMETA) and event.key == K_q) or (event.type == KEYUP and event.mod in (KMOD_ALT, KMOD_LALT, KMOD_RALT) and event.key == K_F4): # Mac: Meta-Q; Windows/Linux: Alt-F4
        # Quit pretty
        killMe = True
        break;
      if event.type == KEYDOWN and event.key in moveAmts:
        locx -= moveAmts[event.key][0]
        locy += moveAmts[event.key][1]
        changed = True
      if event.type == MOUSEBUTTONDOWN:
        pos = event.pos
        #print locx, locy, pos[0] / bx, pos[1] / by, locx + pos[0] / bx, (yPerScreen + locy) - pos[1] / by
        #print (pos[0] / bx + locx, locy - (pos[1] / by), 0)
        pThread.get((pos[0] / bx + locx, (yPerScreen + locy) - pos[1] / by, 0), True)
    # End For
    #pygame.event.pump()
    # End While
except Exception, ex:
  killMe = True # Signal child threads to die
  print ex
  #raise

pygame.time.delay(500)

while pThread.isAlive():
  print "Waiting for Child Threads to die."
  pygame.time.delay(500)

quitPretty()

pThread.join()

if error:
  print error

print "Main Program dead"